using System;
using System.Threading;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.IO.Ports;
using System.Diagnostics;

namespace Imperium.X10
{
	/// <summary>
	/// Enables control of the X10 CM11A device.
	/// </summary>
	public sealed class CM11A : IDisposable
    {
        #region Declarations

        static Dictionary<string, CM11A> instances = new Dictionary<string, CM11A>();
        System.IO.Ports.SerialPort serialPort;
        string portName = null;
        int refCount = 0;
        int baudRate = 4800;
        int dataBits = 8;
        int readTimeout = 2500;
        StopBits stopBits = StopBits.One;
        X10HouseCode houseCodeToMonitor = X10HouseCode.A;
        int defaultDimBrightAmount = 15;

        const byte deviceReadyCode = 0x55;
        const byte acknowledgeCode = 0x00;
        const byte addressNotificationCode = 0x04;
        const byte functionNotificationBaseCode = 0x06;
        const byte powerFailureCode = 0xA5;
        const byte pollSignalCode = 0x5A;
        const byte pollAcknowledgementCode = 0xC3;

        // Codes for the house codes and unit codes are identical
        readonly byte[] addressCodes = new byte[] { 0x06, 0x0E, 0x02, 0x0A, 0x01, 0x09, 0x05, 0x0D, 0x07, 0x0F, 0x03, 0x0B, 0x00, 0x08, 0x04, 0x0C };
        readonly byte[] functionCodes = new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06 };

        #endregion

        #region Constructor and Instance Methods

        /// <summary>
        /// Creates the CM11A device and opens the specified port for communication.
        /// </summary>
        /// <param name="portName">The name of the port to open, such as "COM1".</param>
        private CM11A(string portName)
        {
            // Set the port properties and open it.
            this.portName = portName;
            serialPort = new SerialPort( portName );
            serialPort.BaudRate = baudRate;
            serialPort.DataBits = dataBits;
            serialPort.StopBits = stopBits;
            serialPort.ReadTimeout = readTimeout;

            // Open the port and initialize the device
            serialPort.Open();
            serialPort.DiscardInBuffer();
            serialPort.DiscardOutBuffer();

            // Subscribe to the PinChanged event to check for Ring
            serialPort.PinChanged += serialPort_PinChanged;
            serialPort.DataReceived += serialPort_DataReceived;
        }

        void serialPort_DataReceived( object sender, SerialDataReceivedEventArgs e )
        {
            Debug.Write( "Data Received" );
        }

        /// <summary>
        /// Gets an instance the CM11A device and opens the specified port for communication.
        /// </summary>
        /// <param name="portName">The name of the port to open, such as COM1.</param>
        public static CM11A Instance(string portName)
        {
            // Check arguments
			if (string.IsNullOrEmpty(portName))
				throw new ArgumentNullException("portName", "portName must not be null or empty.");

			// Lock to make thread-safe
            lock (instances)
            {
                CM11A instance = null;

                if (instances.ContainsKey(portName))
                {
                    instance = instances[portName];
                }
                else
                {
                    try
                    {
                        // Create an instance
                        instance = new CM11A(portName);
                    }
                    catch (Exception ex)
                    {
                        // There was a problem communicating with the device.
                        instance = null;
                        throw new IOException(String.Format("There was an error creating the CM11A device on {0}.", portName), ex);
                    }
                    finally
                    {
                        if (instance != null)
                        {
                            // Increase the reference counter and add to the hashtable
                            Interlocked.Increment(ref instance.refCount);
                            instances.Add(portName, instance);
                        }
                    }
                }

                return instance;
           }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Sends a command to the CM11A device for a specific house code and unit code.
        /// </summary>
        /// <param name="houseCode">The house code of the unit to control.</param>
        /// <param name="unitCode">The number (1-16) of the unit to control.</param>
        /// <param name="command">The command to send to the specified unit.</param>
        public void SendCommand(X10HouseCode houseCode, int unitCode, X10Command command)
        {
            SendCommand(houseCode, unitCode, command, defaultDimBrightAmount);
        }

        /// <summary>
        /// Sends a command to the CM11A device for a specific house code and unit code.
        /// </summary>
        /// <param name="houseCode">The house code of the unit to control.</param>
        /// <param name="unitCode">The number (1-16) of the unit to control.</param>
        /// <param name="command">The command to send to the specified unit.</param>
        /// <param name="dimOrBrightAmount">The percentage (1-100) the specified unit will be dimmed or brightened.</param>
        public void SendCommand(X10HouseCode houseCode, int unitCode, X10Command command, int dimOrBrightAmount)
        {
            #region Parameter Checking

            // Check device code
            if (unitCode < 1 || unitCode > 16)
                throw new ArgumentOutOfRangeException("unitCode", unitCode, "The unit code must be between 1 - 16.");

            if (dimOrBrightAmount < 1 || dimOrBrightAmount > 100)
                throw new ArgumentOutOfRangeException("dimOrBrightAmount", dimOrBrightAmount, "dimOrBrightAmount must be between 1 - 100");
            
            #endregion

            byte[] address = CreateAddress(houseCode, unitCode);
            byte[] function = CreateFunction(houseCode, command, dimOrBrightAmount);

            byte addressChecksum = CreateChecksum( address );
            byte functionChecksum = CreateChecksum( function );

            bool success = false;
            int numAttempts = 0;

            // Used to prevent multiple threads from sending commands at the same time.
            object deviceLock = new object();

            lock ( deviceLock )
            {                 
                do
                {
                    // Only attempt to write to the device 10 times
                    numAttempts++;
                    if ( numAttempts > 10 )
                        throw new IOException( "The CM11A device was unable to send the requested address." );
                    try
                    {
                        if ( SendCommandBytes( address ) )
                            success = SendCommandBytes( function );
                    }
                    catch { }

                }
                while ( !success ); // Do this until both the address and function are sent successfully
            }
        }

        
        #endregion

        #region IDisposable Members

        /// <summary>
        /// Disposes of the CM11A object and closes the serial port.
        /// </summary>
        public void Dispose()
        {
            // If no references to the class exist, delete it and remove it from the dictionary
            if (Interlocked.Decrement( ref refCount ) == 0)
            {
                // Remove the instance from the hashtable and then dispose of it
                //lock (instances)
                {
                    instances.Remove( portName );
                }

                RealDispose();
            }
        }

        private void RealDispose()
        {
            GC.SuppressFinalize( this );

            if (serialPort != null)
            {
                if (serialPort.IsOpen)
                    serialPort.Close();

                serialPort = null;
            }
        }


        #endregion

        #region Helper Methods

        private bool SendCommandBytes( byte[] command )
        {
            // Get the checksum for this command
            byte checksum = CreateChecksum( command );

            // Clear the incoming buffer
            serialPort.DiscardInBuffer();

            // Send the command and get the response
            serialPort.Write( command, 0, 2 );
            byte response = (byte)serialPort.ReadByte();
            bool success = ( checksum == response );

            // If there wasn't success, handle the response
            if ( !success )
                HandleResponse( response );
            else
            {
                // Acknowledge and receive ready code
                serialPort.Write( new byte[] { acknowledgeCode }, 0, 1 );
                serialPort.ReadByte();
            }

            return success;
        }

        private byte CreateChecksum( byte[] command )
        {
            return Convert.ToByte( ( command[0] + command[1] ) & 0xff );
        }

        private byte[] CreateAddress( X10HouseCode houseCode, int unitCode )
        {
            // Assemble the address (4 bit house code then 4 bit unit code) of the device we're addressing
            byte houseCodeByte = addressCodes[(int)houseCode];
            byte unitCodeByte = addressCodes[unitCode - 1];
            byte address = Convert.ToByte( ( houseCodeByte << 4 ) | unitCodeByte ); // Move the house code to the left by four bits and append the unit code as the right four bits.

            return new byte[] { addressNotificationCode, address };
        }

        private byte[] CreateFunction( X10HouseCode houseCode, X10Command command, int dimOrBrightAmount )
        {
            // Assemble the function (4 bit house code then 4 bit function code)
            byte houseCodeByte = addressCodes[(int)houseCode];
            byte functionCodeByte = functionCodes[(int)command];
            byte function = Convert.ToByte( ( houseCodeByte << 4 ) | functionCodeByte ); // Move the house code to the left by four bits and append the function code as the right four bits.
            byte functionNotification = new byte();

            if ( command == X10Command.Brighten || command == X10Command.Dim )
            {
                // Normalize the dim/brighten amount to send. The values are integers between 0 and 22.
                byte normalizedDimBrighten = Convert.ToByte( Math.Floor( dimOrBrightAmount * .01 * 22 ) );
                functionNotification = Convert.ToByte( ( normalizedDimBrighten << 3 ) | functionNotificationBaseCode ); // The dim amount is the left five bits and the functionNotificationBase is the right three bits.
            }
            else
            {
                functionNotification = functionNotificationBaseCode; // We don't send a dim/brighten amount, just the function notification bits.
            }

            // Calculate the checksum for this function
            byte checksum = Convert.ToByte( ( functionNotification + function ) & 0xff );

            return new byte[] { functionNotification, function };
        }


        private void HandleResponse(byte response)
        {
            // Figure out what type of response was sent
            switch (response)
            {
                case powerFailureCode:
                    SetClock();
                    break;

                case pollSignalCode:
                    HandleIncomingData();
                    break;
            }
        }

        private void AcknowledgePoll()
        {
            serialPort.Write( new byte[] { pollAcknowledgementCode }, 0, 1 );
        }

        private void HandleIncomingData()
        {
            object deviceLock = new object();

            lock ( deviceLock )
            {
                byte rawDataLength;

                do
                {
                    // Sometimes the acknowledge doesn't work right away. Loop until it does.
                    AcknowledgePoll();
                    rawDataLength = (byte)serialPort.ReadByte();
                }
                while ( rawDataLength == pollSignalCode );

                // Read the first byte. It tells us how many bytes of data will follow
                byte dataLength = (byte)( rawDataLength - 1 ); // Don't account for the address/function mask byte, which is the next byte

                // Read the address/function mask byte
                byte addressFunctionMask = (byte)serialPort.ReadByte();

                // Read the remaining data buffer into a list of bytes -- should be a max of 8 more bytes.
                List<byte> dataBuffer = new List<byte>();
                for ( int i = 0; i <= dataLength - 1; i++ )
                {
                    dataBuffer.Add( (byte)serialPort.ReadByte() );
                }
            }
        }

        private void SetClock()
        {
            Trace.WriteLine( "Set Clock" );

            DateTime now = DateTime.Now;
            byte[] clockCommand = new byte[7];
            clockCommand[0] = 0x9b;
            clockCommand[1] = (byte)now.Second;
            int totalMinutes = (now.Hour * 60) + now.Minute;
            clockCommand[2] = (byte)(totalMinutes % 120); // Minutes 0-119
            clockCommand[3] = (byte)(now.Hour / 2); // Hours, no am/pm
            clockCommand[4] = (byte)(now.DayOfYear & 0xff); // Day of year (first 8 bits)
            clockCommand[5] = (byte)(2 ^ (int)now.DayOfWeek);

            // Set the high bit if the day of the year is beyond 255
            if (now.DayOfYear > 255)
            {
                clockCommand[5] = (byte)(clockCommand[5] | 0x80);
            }

            clockCommand[6] = (byte)(addressCodes[(int)houseCodeToMonitor] << 4); // House code to monitor

            // Send the clock command and read its response. Is this a checksum?
            serialPort.Write(clockCommand, 0, 7);
            serialPort.ReadByte();

            // Send an acknowledge code and read back the response
            serialPort.Write(new byte[] { acknowledgeCode }, 0, 1);
            serialPort.ReadByte();
        }

        #endregion

        #region Properties

        /// <summary>
        /// Communication port where the CM11A device is attached.
        /// </summary>
        public string PortName
        {
            get
            {
                return this.portName;
            }
        }

        #endregion

        #region Serial Port Event Handlers

        private void serialPort_PinChanged( object sender, SerialPinChangedEventArgs e )
        {
            if ( e.EventType == SerialPinChange.Ring )
            {
                Debug.WriteLine( "RING!!!" );

                byte response = (byte)serialPort.ReadByte();

                HandleResponse( response );
            }
        }

        #endregion
    }
}
